/**
 * @module io
 * @description
 * 这个模块处理所有与输入/输出（I/O）相关的操作。
 * 包括从游戏世界中导出地形数据到 `TerrainSchematic` 实例，
 * 以及将 `TerrainSchematic` 实例的数据导入回游戏世界。
 * 同时还包含了与剪贴板交互的功能。
 */

import type TerrainSchematic from "../schematic";
import { CompressedTerrainData, SchematicBlock } from "../types";

/**
 * 获取当前实例中完整的、可序列化的蓝图数据。
 * 这个方法会将内部高效的方块列表转换回原始的图层字符串格式。
 * @returns 一个 `CompressedTerrainData` 对象，可以被 `JSON.stringify` 并保存。
 */
export function getData(schematic: TerrainSchematic): CompressedTerrainData {
  const size = schematic.getDimensions();
  const blocksByLayer: {
    offsetX: number;
    offsetZ: number;
    paletteIndex: number;
  }[][] = Array.from({ length: size.y }, () => []);

  for (const block of schematic.schematicBlocks) {
    blocksByLayer[block.y].push({
      offsetX: block.x,
      offsetZ: block.z,
      paletteIndex: block.p,
    });
  }

  const layers = blocksByLayer.map((layerBlocks) =>
    layerBlocks
      .map((b) => `${b.offsetX},${b.offsetZ},${b.paletteIndex}`)
      .join(";")
  );

  return {
    ...schematic.data,
    layers: layers,
  };
}

/**
 * 从世界中捕获或重新捕获一个区域的地形，并更新当前实例的数据。
 * 这个方法会覆盖实例中任何现有的蓝图数据。
 * @param bounds 包含lo和hi两个点的边界框，定义了要捕获的区域。
 * @returns `this`，允许链式调用。
 * @throws 如果边界坐标无效（例如小于0或大于世界边界），则抛出错误。
 */
export function exportFromWorld(
  schematic: TerrainSchematic,
  bounds: GameBounds3
): TerrainSchematic {
  const start = bounds.lo;
  const end = bounds.hi;
  if (start.x < 0 || start.y < 0 || start.z < 0) {
    throw new Error("地图边界不能小于0");
  }

  schematic.origin = start.clone();
  const palette: { id: voxelId; rot: voxelRotation }[] = [];
  const paletteMap = new Map<string, number>();
  schematic.schematicBlocks = [];

  for (let { y } = start; y <= end.y; y++) {
    for (let { z } = start; z <= end.z; z++) {
      for (let { x } = start; x <= end.x; x++) {
        const block = {
          id: voxels.getVoxelId(x, y, z),
          rot: voxels.getVoxelRotation(x, y, z),
        };

        if (block.id === 0) {
          continue;
        }

        const key = `${block.id}:${block.rot}`;
        if (!paletteMap.has(key)) {
          paletteMap.set(key, palette.length);
          palette.push(block);
        }
        const paletteIndex = paletteMap.get(key)!;

        schematic.schematicBlocks.push({
          x: x - start.x,
          y: y - start.y,
          z: z - start.z,
          p: paletteIndex,
        });
      }
    }
  }

  const flattenedPalette = palette.flatMap((p) => [p.id, p.rot]);

  schematic.data = {
    bounds: new GameBounds3(new GameVector3(0, 0, 0), end.sub(start)),
    palette: flattenedPalette,
  };

  return schematic;
}

/**
 * 将当前实例中存储的蓝图数据导入（放置）到游戏世界中。
 * @param position 可选，蓝图在世界中的目标坐标。所有方块将以此为基点进行放置。如果未提供，则使用蓝图的边界框的lo点。
 * - 基点图示：https://static.codemao.cn/pickduck/ByD_8aUHlg.png
 * @returns `this`，允许链式调用。
 */
export function importToWorld(
  schematic: TerrainSchematic,
  position?: GameVector3
): TerrainSchematic {
  // 确定最终的放置原点。如果设置了轴心，则放置的是轴心点。
  let basePosition = position || schematic.origin;
  if (schematic.pivot) {
    basePosition = basePosition.sub(schematic.pivot);
  }

  const { palette: flattenedPalette } = schematic.data;

  if (!flattenedPalette) {
    console.error("导入失败：数据格式无效，缺少 `palette`。");
    return schematic;
  }

  // 从平铺的数组中重建调色板
  const palette: { id: voxelId; rot: voxelRotation }[] = [];
  for (let i = 0; i < flattenedPalette.length; i += 2) {
    palette.push({
      id: flattenedPalette[i] as voxelId,
      rot: flattenedPalette[i + 1] as voxelRotation,
    });
  }

  for (const block of schematic.schematicBlocks) {
    const blockType = palette[block.p];
    if (blockType) {
      const worldX = basePosition.x + block.x;
      const worldY = basePosition.y + block.y;
      const worldZ = basePosition.z + block.z;
      voxels.setVoxel(worldX, worldY, worldZ, blockType.id, blockType.rot);
    }
  }
  return schematic;
}

/**
 * 异步地、非阻塞地将当前蓝图导入到世界中。
 * 这个方法会通过监听游戏刻（tick）事件来分批放置方块，以防止游戏主线程长时间阻塞而导致卡顿。
 * @param position 可选，蓝图在世界中的目标坐标。所有方块将以此为基点进行放置。如果未提供，则使用蓝图的边界框的lo点。
 * - 基点图示：https://static.codemao.cn/pickduck/ByD_8aUHlg.png
 * @param blocksPerTick 每一游戏tick（帧）放置的方块数量。数量越少，游戏越流畅，但总耗时越长。
 * @returns 一个在所有方块都放置完毕后解析的 Promise，其值为 `this`。
 */
export function importToWorldAsync(
  schematic: TerrainSchematic,
  position?: GameVector3,
  blocksPerTick = 20
): Promise<TerrainSchematic> {
  return new Promise((resolve, reject) => {
    // 确定最终的放置原点。如果设置了轴心，则放置的是轴心点。
    let basePosition = position || schematic.origin;
    if (schematic.pivot) {
      basePosition = basePosition.sub(schematic.pivot);
    }

    const { palette: flattenedPalette } = schematic.data;
    if (!flattenedPalette) {
      return reject(new Error("无效的调色板数据"));
    }

    const palette: { id: voxelId; rot: voxelRotation }[] = [];
    for (let i = 0; i < flattenedPalette.length; i += 2) {
      palette.push({
        id: flattenedPalette[i] as voxelId,
        rot: flattenedPalette[i + 1] as voxelRotation,
      });
    }

    let currentIndex = 0;
    let subscription: GameEventHandlerToken | null = null;

    const tickHandler = () => {
      try {
        const blocksToPlaceThisTick = Math.min(
          blocksPerTick,
          schematic.schematicBlocks.length - currentIndex
        );

        for (let i = 0; i < blocksToPlaceThisTick; i++) {
          const block = schematic.schematicBlocks[currentIndex];
          if (block) {
            const blockType = palette[block.p];
            if (blockType) {
              const worldX = basePosition.x + block.x;
              const worldY = basePosition.y + block.y;
              const worldZ = basePosition.z + block.z;
              voxels.setVoxel(
                worldX,
                worldY,
                worldZ,
                blockType.id,
                blockType.rot
              );
            }
          }
          currentIndex++;
        }

        if (currentIndex >= schematic.schematicBlocks.length) {
          if (subscription) {
            subscription.cancel();
          }
          resolve(schematic);
        }
      } catch (e) {
        if (subscription) {
          subscription.cancel();
        }
        reject(e);
      }
    };

    subscription = world.onTick(tickHandler);
  });
}
